Some compilers for object-oriented languages such as C++ generate functions called thunks as an optimization of virtual function calls in the presence of multiple or virtual inheritance. Consider the C++ code
struct A { int value; virtual int access() { return this->value; } }; struct B { int value; virtual int access() { return this->value; } }; struct C : public A, public B { int better_value; virtual int access() { return this->better_value; } }; int use(B *b) { return b->access(); } ... C c; use(&c); ...
Since the function B::access
is virtual, a call to b->access()
requires use of a vtable dispatch. In naïve implementations, the dispatch will consist of five steps:[1]:section 5.1
b
holds a pointer to the vtable. Load that pointer into a register.B
contains an entry (dispatch) for the method B::access
. Find that entry E.C::access
). Load that function pointer.C::access
expects a this
pointer to an instance of class C
. But b
points to an instance of class B
. So we must decrement b
by the offset of B
in C
(in this example, by the size of C::A::value
plus the size of C::A
's vtable pointer). Since this offset is not known to the method use
at compile time, it must also be loaded from the vtable entry E.C::access
with the adjusted value of b
.The fourth step, in which an offset (a negative offset in this example) is loaded from E and added to b
, can be completely eliminated by the compiler, thus speeding up every virtual method call, if the compiler generates a wrapper function like this, and places its address in the vtable entry E:
int thunk_for_C_access_in_B(B *b) { C *adjusted_b = (C *)b; /* decrements b by the appropriate offset, so that it points to a C object */ return adjusted_b->C::access(); /* a tail call to the original method C::access() */ }
Then the steps for b->access()
become:
b
holds a pointer to the vtable. Load that pointer into a register.B::access
is at some known offset in the vtable for B
; find that entry E.thunk_for_C_access_in_B
). Load that function pointer W.b
. If b
was really of dynamic type B
, then W = B::access
, and so we have saved two instructions (an expensive memory load and a cheap addition). If b
was really of dynamic type C
, then W = thunk_for_C_access_in_B
, and so we have added one instruction (a cheap unconditional branch at the end of thunk_for_C_access_in_B
).Since the particular pattern of multiple inheritance in class C
is rare in practice, we will generally save more instructions than we add. At the same time, we no longer need to store an offset for each entry E in the vtable, and so we have halved the size of every vtable in the program.
The term "thunk" for these compiler-generated functions can be seen as an example of "thunk", meant as a nullary function (one with no parameters).[2]:page 3 It could have been described simply as a compiler-generated wrapper function, but the term "thunk" for these functions is now established as convention.